﻿#region Usings

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;

#endregion

namespace SilverBox.Controls
{
    public delegate void SelectedItemChangedEvent(CoverFlowEventArgs e);

    public class CoverFlowControl : ItemsControl, INotifyPropertyChanged
    {
        public static readonly DependencyProperty CanvasXProperty =
            DependencyProperty.Register("CanvasX", typeof (double), typeof (CoverFlowControl),
                                        new PropertyMetadata(0d, OnCanvasXChanged));

        private readonly List<CoverFlowItemControl> items;
        private readonly List<CoverFlowItemControl> tempItems;

        private Dictionary<object, CoverFlowItemControl> _objectToItemContainer;
        private double canvasX;
        private DoubleAnimation canvasXAnimation;
        private bool enableDrag = true;
        private TextBlock info;
        private bool isAnimating;

        private bool isDragging;
        private ItemsPresenter ItemsPresenter;
        private FrameworkElement LayoutRoot;
        private int m;
        private double middle;
        private bool mouseDown;
        private Point offset;
        private Storyboard sb;
        private int selectedIndex;
        private int si;
        private double sid;
        private DateTime startTime;

        private double travel;

        public CoverFlowControl()
        {
            DefaultStyleKey = typeof (CoverFlowControl);
            items = new List<CoverFlowItemControl>();
            tempItems = new List<CoverFlowItemControl>();
            SizeChanged += CoverFlowControl_SizeChanged;
        }

        public bool EnableDrag
        {
            get { return enableDrag; }
            set { enableDrag = value; }
        }

        public int PageCount { get; set; }

        public int SelectedIndex
        {
            get { return selectedIndex; }
            set
            {
                if (items.Count == 0)
                    return;
                if (value < 0)
                    selectedIndex = 0;
                else if (value >= items.Count)
                    selectedIndex = items.Count - 1;
                else
                    selectedIndex = value;
                Animate(l*selectedIndex);

                var e = new CoverFlowEventArgs {Index = selectedIndex, Item = items[selectedIndex].Content};
                if (SelectedItemChanged != null)
                    SelectedItemChanged(e);
                OnPropertyChanged("SelectedIndex");
                OnPropertyChanged("SelectedItem");
            }
        }

        public object SelectedItem
        {
            get { return items.Count > 0 ? items[SelectedIndex].Content : null; }
            set
            {
                CoverFlowItemControl o = GetItemContainerForObject(value);
                if (o != null)
                    SelectedIndex = items.IndexOf(o);
            }
        }

        public double CanvasX
        {
            get { return (double) GetValue(CanvasXProperty); }
            set { SetValue(CanvasXProperty, value); }
        }

        public double M { get;
            //{
            //    return middle - canvasX;
            //}
            set; }

        private double k
        {
            get { return SpaceBetweenSelectedItemAndItems; }
        }

        private double l
        {
            get { return SpaceBetweenItems; }
        }

        private double r
        {
            get { return RotationAngle; }
        }

        private double z
        {
            get { return ZDistance; }
        }

        private double s
        {
            get { return Scale; }
        }

        #region INotifyPropertyChanged Members

        public event PropertyChangedEventHandler PropertyChanged;

        #endregion

        public event SelectedItemChangedEvent SelectedItemChanged;
        public event SelectedItemChangedEvent SelectedItemClicked;

        private static void OnCanvasXChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            (d as CoverFlowControl).SetItemPositions(false);
        }

        public void SetItemPositions(bool setCanvas)
        {
            if (CanvasX < 0)
                CanvasX = 0;
            else if (items.Count > 0 && CanvasX > (items.Count - 1)*l)
                CanvasX = (items.Count - 1)*l;
            canvasX = middle - CanvasX;
            M = middle - canvasX;

            if (ItemsPresenter != null)
                Canvas.SetLeft(ItemsPresenter, canvasX);

            // this will be the item currently closest to the center position
            sid = M/l;

            // the index of the item closest to the center postion
            si = (int) Math.Round(sid);

            m = (int) Math.Round(middle/l);

            // hide all past items
            foreach (var item in tempItems.ToArray())
                item.Visibility = Visibility.Collapsed;
            tempItems.Clear();

            // only adjust the ones that are currently on the screen
            for (int i = si - m < 0 ? 0 : si - m; i < si + m && i < items.Count; i++)
            {
                SetLocation(items[i], i, setCanvas);
            }
        }

        private void CoverFlowControl_MouseMove(object sender, MouseEventArgs e)
        {
            if (mouseDown)
            {
                isDragging = true;
                // Where is the mouse now?

                Point newPosition = e.GetPosition(LayoutRoot);


                double of = newPosition.X - offset.X;
                if ((of < 0 && travel < 0) || (of > 0 && travel > 0))
                    travel += of;
                else
                {
                    travel = of;
                    startTime = DateTime.Now;
                }

                CanvasX = CanvasX - of;
                offset.X = newPosition.X;
            }
        }

        private void CoverFlowControl_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            if (isAnimating)
                sb.Pause();
            // Mark that we're doing a drag
            mouseDown = true;

            // Ensure that the mouse can't leave the dragon
            //LayoutRoot.CaptureMouse();

            // Determine where the mouse 'grabbed' 
            // to use during MouseMove
            offset = e.GetPosition(LayoutRoot);
            travel = 0;
        }

        private void CoverFlowControl_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            if (isDragging)
            {
                TimeSpan span = DateTime.Now - startTime;
                // get the volocity and cut it in half
                double v = (travel/span.TotalSeconds)/2;
                double test = l*3;
                if (v > test || v < -test)
                {
                    SetDuration(MultiDuration);
                    Animate(CanvasX - v);
                }
                else
                {
                    double sid = M/l;
                    var si = (int) Math.Round(sid);
                    SelectedIndex = si;
                }
                e.Handled = true;
            }
            else
            {
                //offset = e.GetPosition(LayoutRoot);
                //double xclick = 
                var item = sender as CoverFlowItemControl;
                int index = items.IndexOf(item);
                if (index == SelectedIndex)
                {
                    if (SelectedItemClicked != null)
                    {
                        var args = new CoverFlowEventArgs {Index = index, Item = item.Content};
                        SelectedItemClicked(args);
                    }
                }
                else
                {
                    if (Math.Abs(SelectedIndex - index) > 1)
                        SetDuration(MultiDuration);
                    else
                        SetDuration(SingleItemDuration);
                    SelectedIndex = index;
                }
                e.Handled = true;
            }
            isDragging = mouseDown = false;
            //LayoutRoot.ReleaseMouseCapture();
        }

        private void Animate(double to)
        {
            canvasXAnimation.To = to;
            isAnimating = true;
            sb.Begin();
        }

        private void CoverFlowControl_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            var visibleArea = new RectangleGeometry();
            var clip = new Rect(0, 0, ActualWidth, ActualHeight);
            foreach (var item in items)
                item.Height = ActualHeight;
            visibleArea.Rect = clip;
            Clip = visibleArea;

            if (ActualWidth > 0 && middle != ActualWidth/2)
            {
                middle = ActualWidth/2;
                canvasX = middle - CanvasX;
                M = middle - canvasX;
                PageCount = (int) Math.Ceiling((middle - k)/l);

                SetItemPositions(true);
                if (items.Count > 0)
                {
                    SelectedIndex = (int) Math.Round(M/l);
                }
            }
        }

        private static void OnValuesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            (d as CoverFlowControl).SetItemPositions(true);
        }

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
            LayoutRoot = (FrameworkElement) GetTemplateChild("LayoutRoot");
            ItemsPresenter = (ItemsPresenter) GetTemplateChild("ItemsPresenter");
            info = (TextBlock) GetTemplateChild("info");


            SingleItemDuration = new Duration(TimeSpan.FromSeconds(0.6));
            MultiDuration = new Duration(TimeSpan.FromSeconds(0.9));
            canvasXAnimation = new DoubleAnimation();
            canvasXAnimation.Duration = SingleItemDuration;
            canvasXAnimation.EasingFunction = new CubicEase();

            sb = new Storyboard();
            sb.Duration = SingleItemDuration;
            sb.Children.Add(canvasXAnimation);
            Storyboard.SetTarget(canvasXAnimation, this);

            Storyboard.SetTargetProperty(canvasXAnimation, new PropertyPath("CanvasX"));
            sb.Completed += sb_Completed;

            Resources.Add("canvasXSb", sb);
            LayoutRoot.KeyDown += OnKeyDown;

            CanvasX = 0;
        }

        private void sb_Completed(object sender, EventArgs e)
        {
            isAnimating = false;
            double sid = M/l;
            var si = (int) Math.Round(sid);
            SetDuration(SingleItemDuration);
            if (SelectedIndex != si)
                SelectedIndex = si;
        }

        protected override DependencyObject GetContainerForItemOverride()
        {
            var item = new CoverFlowItemControl();
            return item;
        }

        protected override bool IsItemItsOwnContainerOverride(object item)
        {
            return (item is CoverFlowItemControl);
        }

        protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
        {
            base.PrepareContainerForItemOverride(element, item);
            var item2 = element as CoverFlowItemControl;
            if (item2 != item)
                ObjectToItemContainer[item] = item2;

            if (!items.Contains(item2))
            {
                int index = items.Count;
                items.Add(item2);
                item2.SizeChanged += item2_SizeChanged;
                item2.MouseLeftButtonUp += CoverFlowControl_MouseLeftButtonUp;
                item2.MouseLeftButtonDown += CoverFlowControl_MouseLeftButtonDown;
                item2.MouseMove += CoverFlowControl_MouseMove;
                SetLocation(item2, index, true);
            }
            if (items.Count == 1)
                SelectedIndex = 0;
        }

        private void item2_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            var item = sender as CoverFlowItemControl;
            int index = items.IndexOf(item);
            if (index == SelectedIndex)
            {
                if (SelectedItemClicked != null)
                {
                    var args = new CoverFlowEventArgs {Index = index, Item = item.DataContext};
                    SelectedItemClicked(args);
                }
            }
            else
            {
                if (Math.Abs(SelectedIndex - index) > 1)
                    SetDuration(MultiDuration);
                else
                    SetDuration(SingleItemDuration);
                SelectedIndex = index;
            }
        }

        private void item2_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            var item = sender as CoverFlowItemControl;
            item.Height = ActualHeight;
            SetLocation(item, items.IndexOf(item), true);
        }

        protected override void ClearContainerForItemOverride(DependencyObject element, object item)
        {
            base.ClearContainerForItemOverride(element, item);
            var item2 = element as CoverFlowItemControl;
            //if (item2 != item)
            //{
            //    this.ObjectToItemContainer.Remove(item);
            //}
            items.Remove(item2);
        }

        private void SetLocation(CoverFlowItemControl item, int index, bool setCanvas)
        {
            if (setCanvas)
                Canvas.SetLeft(item, index*l - item.XOffset);
            if (si - PageCount > index || index > si + PageCount)
            {
                item.Visibility = Visibility.Collapsed;
                return;
            }
            tempItems.Add(item);
            item.Visibility = Visibility.Visible;

            double t = -(sid - index);
            double tk = k;
            double tr = r;
            double ts = s;
            double tz = z;
            int d = index - si;

            if (t < 1 && t > -1)
            {
                tk *= t;
                tr *= t;
                double tab = Math.Abs(t);
                ts += (1 - s)*(1 - tab);
                tz *= tab;
            }
            else if (t < 0)
            {
                tk = -tk;
                tr = -tr;
                d = si - index;
            }
            if (item.TranslateTransform != null)
                item.TranslateTransform.X = tk;

            if (item.PlaneProjection != null)
            {
                item.PlaneProjection.RotationY = tr;
                item.PlaneProjection.LocalOffsetZ = tz;
            }

            if (item.ScaleTransform != null)
            {
                item.ScaleTransform.ScaleX = ts;
                item.ScaleTransform.ScaleY = ts;
            }

            Canvas.SetZIndex(item, -d);
        }

        private void SetDuration(Duration duration)
        {
            sb.Duration = duration;
            canvasXAnimation.Duration = duration;
        }

        public void NextItem()
        {
            if (SelectedIndex < items.Count - 1)
            {
                SetDuration(SingleItemDuration);
                SelectedIndex++;
            }
        }

        public void PreviousItem()
        {
            if (SelectedIndex > 0)
            {
                SetDuration(SingleItemDuration);
                SelectedIndex--;
            }
        }

        public void NextPage()
        {
            if (SelectedIndex == items.Count - 1)
                return;

            SetDuration(MultiDuration);
            int i = PageCount;
            if (SelectedIndex + i >= items.Count)
                SelectedIndex = items.Count - 1;
            else
                SelectedIndex += i;
        }

        public void PreviousPage()
        {
            if (SelectedIndex == 0)
                return;
            SetDuration(MultiDuration);
            int i = PageCount;
            if (SelectedIndex - i < 0)
                SelectedIndex = 0;
            else
                SelectedIndex -= i;
        }

        public void First()
        {
            if (items.Count == 0)
                return;
            SetDuration(MultiDuration);
            SelectedIndex = 0;
        }

        public void Last()
        {
            if (items.Count == 0)
                return;
            SetDuration(MultiDuration);
            SelectedIndex = items.Count - 1;
        }

        public void OnKeyDown(object sender, KeyEventArgs e)
        {
            if (e.Handled)
                return;
            if (e.Key == Key.Right || e.Key == Key.D)
            {
                NextItem();
                e.Handled = true;
            }
            else if (e.Key == Key.Left || e.Key == Key.A)
            {
                PreviousItem();
                e.Handled = true;
            }
            else if (e.Key == Key.PageDown || e.Key == Key.S || e.Key == Key.Down)
            {
                NextPage();
                e.Handled = true;
            }
            else if (e.Key == Key.PageUp || e.Key == Key.W || e.Key == Key.Up)
            {
                PreviousPage();
                e.Handled = true;
            }
            else if (e.Key == Key.Home || e.Key == Key.Q)
            {
                First();
                e.Handled = true;
            }
            else if (e.Key == Key.End || e.Key == Key.E)
            {
                Last();
                e.Handled = true;
            }
        }

        private void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }

        #region ItemContainer methods

        protected Dictionary<object, CoverFlowItemControl> ObjectToItemContainer
        {
            get
            {
                if (_objectToItemContainer == null)
                {
                    _objectToItemContainer = new Dictionary<object, CoverFlowItemControl>();
                }
                return _objectToItemContainer;
            }
        }

        protected CoverFlowItemControl GetItemContainerForObject(object value)
        {
            var item = value as CoverFlowItemControl;
            if (item == null)
            {
                ObjectToItemContainer.TryGetValue(value, out item);
            }
            return item;
        }

        #endregion

        #region SpaceBetweenItems (DependencyProperty) (l)

        public static readonly DependencyProperty SpaceBetweenItemsProperty =
            DependencyProperty.Register("SpaceBetweenItems", typeof (double), typeof (CoverFlowControl),
                                        new PropertyMetadata(60d, OnValuesChanged));

        public double SpaceBetweenItems
        {
            get { return (double) GetValue(SpaceBetweenItemsProperty); }
            set { SetValue(SpaceBetweenItemsProperty, value); }
        }

        #endregion

        #region SpaceBetweenSelectedItemAndItems (DependencyProperty) (k)

        public static readonly DependencyProperty SpaceBetweenSelectedItemAndItemsProperty =
            DependencyProperty.Register("SpaceBetweenSelectedItemAndItems", typeof (double), typeof (CoverFlowControl),
                                        new PropertyMetadata(160d, OnValuesChanged));

        public double SpaceBetweenSelectedItemAndItems
        {
            get { return (double) GetValue(SpaceBetweenSelectedItemAndItemsProperty); }
            set { SetValue(SpaceBetweenSelectedItemAndItemsProperty, value); }
        }

        #endregion

        #region RotationAngle (DependencyProperty) (r)

        public static readonly DependencyProperty RotationAngleProperty =
            DependencyProperty.Register("RotationAngle", typeof (double), typeof (CoverFlowControl),
                                        new PropertyMetadata(45d, OnValuesChanged));

        public double RotationAngle
        {
            get { return (double) GetValue(RotationAngleProperty); }
            set { SetValue(RotationAngleProperty, value); }
        }

        #endregion

        #region ZDistance (DependencyProperty) (z)

        public static readonly DependencyProperty ZDistanceProperty =
            DependencyProperty.Register("ZDistance", typeof (double), typeof (CoverFlowControl),
                                        new PropertyMetadata(0.0d, OnValuesChanged));

        public double ZDistance
        {
            get { return (double) GetValue(ZDistanceProperty); }
            set { SetValue(ZDistanceProperty, value); }
        }

        #endregion

        #region Scale (DependencyProperty) (s)

        public static readonly DependencyProperty ScaleProperty =
            DependencyProperty.Register("Scale", typeof (double), typeof (CoverFlowControl),
                                        new PropertyMetadata(.7d, OnValuesChanged));

        public double Scale
        {
            get { return (double) GetValue(ScaleProperty); }
            set { SetValue(ScaleProperty, value); }
        }

        #endregion

        #region SingleItemDuration (DependencyProperty)

        public static readonly DependencyProperty SingleItemDurationProperty =
            DependencyProperty.Register("SingleItemDuration", typeof (Duration), typeof (CoverFlowControl), null);

        public Duration SingleItemDuration
        {
            get { return (Duration) GetValue(SingleItemDurationProperty); }
            set { SetValue(SingleItemDurationProperty, value); }
        }

        #endregion

        #region MultiDuration (DependencyProperty)

        public static readonly DependencyProperty MultiDurationProperty =
            DependencyProperty.Register("MultiDuration", typeof (Duration), typeof (CoverFlowControl), null);

        public Duration MultiDuration
        {
            get { return (Duration) GetValue(MultiDurationProperty); }
            set { SetValue(MultiDurationProperty, value); }
        }

        #endregion

        #region EasingFunction (DependencyProperty)

        public static readonly DependencyProperty EasingFunctionProperty =
            DependencyProperty.Register("EasingFunction", typeof (IEasingFunction), typeof (CoverFlowControl), null);

        public IEasingFunction EasingFunction
        {
            get { return (IEasingFunction) GetValue(EasingFunctionProperty); }
            set { SetValue(EasingFunctionProperty, value); }
        }

        #endregion
    }
}